Micron Document
🎖️GitЯра🎖️

Commit c655a5015a499ca75c6f69a7b6fa27f73a94d55e


Parents : f230c13
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-08-15T11:55:37-07:00
Committer : GitHub <noreply@github.com>
Date : 2026-08-15T13:55:37-05:00

test: deflake AndroidMeshWorkerManagerTest by rooting the parked worker coroutine (#6720)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Changes
Diff

diff --git a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManagerTest.kt b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManagerTest.kt
index 629bf06049..daa9698e68 100644
--- a/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManagerTest.kt
+++ b/core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/AndroidMeshWorkerManagerTest.kt
@@ -21,12 +21,14 @@ import androidx.test.core.app.ApplicationProvider
import androidx.work.Configuration
import androidx.work.CoroutineWorker
import androidx.work.ListenableWorker
+import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerFactory
import androidx.work.WorkerParameters
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
-import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.repository.PersistedPacketId
@@ -34,7 +36,6 @@ import org.meshtastic.core.service.worker.SendMessageWorker
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
-import kotlin.test.assertFalse
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
@@ -43,10 +44,11 @@ class AndroidMeshWorkerManagerTest {
@Test
fun `repeated persisted row scheduling keeps the active unique worker`() {
val context = ApplicationProvider.getApplicationContext<Context>()
+ val factory = BlockingWorkerFactory()
val configuration =
Configuration.Builder()
.setExecutor(SynchronousExecutor())
- .setWorkerFactory(BlockingWorkerFactory())
+ .setWorkerFactory(factory)
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(context, configuration)
@@ -54,32 +56,56 @@ class AndroidMeshWorkerManagerTest {
val persistedId = PersistedPacketId(myNodeNum = 42, uuid = 99L)
val manager = AndroidMeshWorkerManager(workManager)
- manager.enqueueSendMessage(persistedId)
- val first = workManager.getWorkInfosForUniqueWork(workName(persistedId)).get().single()
- manager.enqueueSendMessage(persistedId)
- val retained = workManager.getWorkInfosForUniqueWork(workName(persistedId)).get().single()
+ try {
+ manager.enqueueSendMessage(persistedId)
+ runBlocking { factory.started.await() }
+ val first = workManager.getWorkInfosForUniqueWork(workName(persistedId)).get().single()
+ assertEquals(WorkInfo.State.RUNNING, first.state, "The worker must be parked before the GC guard runs")
+ // Regression guard (#6720): GC pressure here used to collect the worker's CallbackToFutureAdapter
+ // completer, marking the running work FAILED so the KEEP enqueue below replaced it (flaky on loaded CI).
+ repeat(3) {
+ System.gc()
+ System.runFinalization()
+ }
+ manager.enqueueSendMessage(persistedId)
+ val retained = workManager.getWorkInfosForUniqueWork(workName(persistedId)).get().single()
- assertFalse(first.state.isFinished, "The first request must remain active for KEEP to apply")
- assertEquals(first.id, retained.id, "KEEP must retain the active work request instead of replacing it")
- workManager.cancelUniqueWork(workName(persistedId)).result.get()
+ assertEquals(first.id, retained.id, "KEEP must retain the active work request instead of replacing it")
+ } finally {
+ factory.release.complete(Unit)
+ workManager.cancelUniqueWork(workName(persistedId)).result.get()
+ }
}
private fun workName(id: PersistedPacketId) = "${SendMessageWorker.WORK_NAME_PREFIX}${id.myNodeNum}_${id.uuid}"
private class BlockingWorkerFactory : WorkerFactory() {
+ // Awaiting release (instead of awaitCancellation) keeps the parked worker coroutine strongly reachable;
+ // startWork()'s future holds its completer only weakly, and a GC'd completer fails the work (#6720).
+ val started = CompletableDeferred<Unit>()
+ val release = CompletableDeferred<Unit>()
+
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters,
): ListenableWorker? = if (workerClassName == SendMessageWorker::class.java.name) {
- BlockingWorker(appContext, workerParameters)
+ BlockingWorker(appContext, workerParameters, started, release)
} else {
null
}
}
- private class BlockingWorker(appContext: Context, workerParameters: WorkerParameters) :
- CoroutineWorker(appContext, workerParameters) {
- override suspend fun doWork(): Result = awaitCancellation()
+ private class BlockingWorker(
+ appContext: Context,
+ workerParameters: WorkerParameters,
+ private val started: CompletableDeferred<Unit>,
+ private val release: CompletableDeferred<Unit>,
+ ) : CoroutineWorker(appContext, workerParameters) {
+ override suspend fun doWork(): Result {
+ started.complete(Unit)
+ release.await()
+ return Result.success()
+ }
}
}

Served by rngit 1.5.2 - Generated in 0.04s